package com.leyou.search.service.impl;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.leyou.common.dto.PageDTO;
import com.leyou.common.exception.LyException;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.SkuDTO;
import com.leyou.item.dto.SpecParamDTO;
import com.leyou.item.dto.SpuDTO;
import com.leyou.search.client.GoodsRepository;
import com.leyou.search.dto.SearchParamDTO;
import com.leyou.search.entity.Goods;
import com.leyou.search.service.SearchService;
import com.leyou.starter.elastic.entity.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;

import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import static com.leyou.search.constants.SearchConstants.*;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class SearchServiceImpl implements SearchService {
    @Autowired(required = false)
    GoodsRepository goodsRepository; //继承Repository接口，动态代理实现接口方法，操作索引库
    @Autowired
    ItemClient itemClient; //ItemClient API模块提供接口 ，动态代理实现，操作数据库
    @Override
    public void createIndexAddMapping() {
        //先删除已经存在的索引库
        try {
            goodsRepository.deleteIndex();
        } catch (Exception e) { //自定义异常继承Exception类。把它改为继承RuntimeException
            log.error("删除失败，可能索引库不存在",e);
        }
        //然后创建一个新的索引库，并没有指定库名，原因GoodsRepository继承接口Repository的泛型类Goods @Indices("Goods")指明了库名
        goodsRepository.createIndex("{\n" +
                "  \"settings\": {\n" +
                "    \"analysis\": {\n" +
                "      \"analyzer\": {\n" +
                "        \"my_pinyin\": {\n" +
                "          \"tokenizer\": \"ik_smart\",\n" +
                "          \"filter\": [\n" +
                "            \"py\"\n" +
                "          ]\n" +
                "        }\n" +
                "      },\n" +
                "      \"filter\": {\n" +
                "        \"py\": {\n" +
                "\t\t  \"type\": \"pinyin\",\n" +
                "          \"keep_full_pinyin\": true,\n" +
                "          \"keep_joined_full_pinyin\": true,\n" +
                "          \"keep_original\": true,\n" +
                "          \"limit_first_letter_length\": 16,\n" +
                "          \"remove_duplicated_term\": true\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"mappings\": {\n" +
                "    \"properties\": {\n" +
                "      \"id\": {\n" +
                "        \"type\": \"keyword\"\n" +
                "      },\n" +
                "      \"suggestion\": {\n" +
                "        \"type\": \"completion\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"title\":{\n" +
                "        \"type\": \"text\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"image\":{\n" +
                "        \"type\": \"keyword\",\n" +
                "        \"index\": false\n" +
                "      },\n" +
                "      \"updateTime\":{\n" +
                "        \"type\": \"date\"\n" +
                "      },\n" +
                "      \"specs\":{\n" +
                "        \"type\": \"nested\",\n" +
                "        \"properties\": {\n" +
                "          \"name\":{\"type\": \"keyword\" },\n" +
                "          \"value\":{\"type\": \"keyword\" }\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}");

    }

    @Override
    public void loadData() {
        int page = 1;
        int rows = 100;
        while (true) {
            log.info("开始导入第{}页数据",page);
            // 分页查询已经上架的spu 方法返回数据Spu表：总条数/总页数totalPage/当前页数据items
            PageDTO<SpuDTO> result = itemClient.querySpuByPage(page, rows, true, null, null, null);
            List<SpuDTO> list = result.getItems();

            //遍历Spu集合 ，把SpuDTO通过buildGoods方法转为Goods
            List<Goods> goodsList = list.stream()
                    .map(this::buildGoods)
                    .collect(Collectors.toList());
            // 批量写入Elasticsearch
            this.goodsRepository.saveAll(goodsList);
            log.info("导入第{}页数据结束。", page);
            // 翻页
            page++;

            // 获取总页数
            Long totalPage = result.getTotalPage();
            // 判断是否还有spu没有查询
            if (page > totalPage) {
                // 没有则结束
                break;
            }

        }
    }

    @Override
    public Mono<List<String>> getSuggestion(String key) {
        if (StringUtils.isEmpty(key)) {
            throw new LyException("请求参数不能为空", 400);
        }
        return goodsRepository.suggestBySingleField(SUGGESTION_FIELD, key); //按照suggestion字段，查询补全
    }

    @Override
    public Mono<PageInfo<Goods>> searchGoods(SearchParamDTO searchParamDTO) {
        //创建SearchSourceBuilder查询对象
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //1添加条件
            //1.1 source过滤 删选出查询的字段 第一个参数是包含能被查询 ，第二个查数排除，不被查询。
            //new String[0] 创建一个长度为0的数组 此处起到补参数站位。
        searchSourceBuilder.fetchSource(DEFAULT_SOURCE_FIELD, new String[0]);
            //1.2 查询条件key ，进行了方法抽取。getQueryBuilder(searchParamDTO)方法构造了查询条件
        searchSourceBuilder.query(getQueryBuilder(searchParamDTO));

        //1.3 分页条件
        searchSourceBuilder.size(searchParamDTO.getSize());
        searchSourceBuilder.from(searchParamDTO.getFrom());
            //1.4 排序条件
        Set<String> sortCondition = new HashSet<>(); //HashSet可以接收null值，有且只有一个，数据不重复
        sortCondition.add("price");
        sortCondition.add("sold");
        sortCondition.add("updatetime");
        if (!StringUtils.isEmpty(searchParamDTO.getSortBy()) && sortCondition.contains(searchParamDTO.getSortBy())) {
                //在有排序字段基础上 ，按照降序排序，还是升序排序。
            searchSourceBuilder.sort(searchParamDTO.getSortBy(), searchParamDTO.getDesc() ? SortOrder.DESC : SortOrder.ASC);
        }
            //1.5 高亮 三要素 高亮字段 /开始/结束标签
        searchSourceBuilder.highlighter(new HighlightBuilder()
                .field(DEFAULT_SEARCH_FIELD)
                .preTags(DEFAULT_PRE_TAG)
                .postTags(DEFAULT_POST_TAG));

        //2子进程查询 并返还Mono<PageInfo<Goods>> 查询需要 SearchSourceBuilder查询对象
        return goodsRepository.queryBySourceBuilderForPageHighlight(searchSourceBuilder);
    }


    /**
     * todo 完成 ,未调试。前端未做稍许改动。
     * @param searchParamDTO
     * @return
     */
    @Override
    public Mono<Map<String, List<?>>> listFilter(SearchParamDTO searchParamDTO) {
        //添加查询条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(getQueryBuilder(searchParamDTO));
        //要查询但不显示结果
        searchSourceBuilder.size(0);

        //添加聚合条件
        searchSourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field("brandId"));
        searchSourceBuilder.aggregation(AggregationBuilders.terms("categoryAgg").field("categoryId"));

        //添加规格参数的nested聚合以及对应的子聚合
        searchSourceBuilder.aggregation(
                AggregationBuilders.nested("specsAgg","specs").
                        subAggregation(AggregationBuilders.terms("nameAgg").field("specs.name").size(30).
                                subAggregation(AggregationBuilders.terms("valueAgg").field("specs.value").size(30)))
        );


        //异步聚合处理 TODO 定位
        Mono<Aggregations> aggregationsMono = goodsRepository.aggregationBySourceBuilder(searchSourceBuilder);
        /*aggregationsMono.subscribe() 这种取法，会让主进程等待，返回结果破坏了异步查询。
        mono中map,对每个元素进行运算，mono只有0个或1个元素，等同于 将Aggregations 进行“二次封装”。*/
        return aggregationsMono.map(aggregations -> {
            Map<String,List<?>> filterResult = new LinkedHashMap<>();//HashMap可替换成LinkedHashMap
            //根据聚合名称获取聚合结果 最好使用terms接口 ，适配高，可获取桶数组
            Terms brandAgg = aggregations.get("brandAgg");
            List<Long> brandIds = brandAgg.getBuckets()
                    .stream()
                    .map(bucket -> ((Terms.Bucket) bucket).getKeyAsNumber())
                    .map(Number::longValue)
                    .collect(Collectors.toList());
            if (!CollectionUtils.isEmpty(brandIds)) {
                filterResult.put("品牌", itemClient.queryByBrandIdList(brandIds));
            }

            // 同理可得 分类聚和结果及其处理
            Terms categoryAgg = aggregations.get("categoryAgg");
            List<Long> categoryIds = categoryAgg.getBuckets()
                    .stream()
                    .map(bucket -> ((Terms.Bucket) bucket).getKeyAsNumber())
                    .map(Number::longValue)
                    .collect(Collectors.toList());
            //BrandIDs获取品牌集合
            if (!CollectionUtils.isEmpty(categoryIds)) {
                filterResult.put("品牌", itemClient.queryCategoryByIds(categoryIds));
            }
            //根据聚合名称 获取 nested集合
            Nested specsAgg = aggregations.get("specsAgg");//Nested 核心包search.aggregations.bucket.nested.Nested
            //根据聚合名称获取specs.name聚合
            Terms nameAgg = specsAgg.getAggregations().get("nameAgg");
            //根据聚合名称获取 vaule聚合
            nameAgg.getBuckets().forEach(bucket ->{
                String resultKey = ((Terms.Bucket) bucket).getKeyAsString();
                Terms valueAgg = ((Terms.Bucket) bucket).getAggregations().get("valueAgg");
                List<String> valueList = valueAgg.getBuckets().stream().map(valueBucket -> {
                    return ((Terms.Bucket) valueBucket).getKeyAsString();
                }).collect(Collectors.toList());

                filterResult.put(resultKey, valueList);
            });

            return filterResult;
        });
    }

    /**
     * 新增商品到索引库
     * @param spuId
     */
    @Override
    public void saveGoodsById(Long spuId) {
        //1.获取spu
        SpuDTO spuDTO = itemClient.queryGoodsById(spuId);
        //2.spudto --- > goods 构件goods
        Goods goods = buildGoods(spuDTO);
        //3.写入索引库
        goodsRepository.save(goods);
    }

    /**
     * 从索引库删除一个商品
     * @param spuId
     */
    @Override
    public void deleteGoodsById(Long spuId) {
        goodsRepository.deleteById(spuId);
    }

    private QueryBuilder getQueryBuilder(SearchParamDTO searchParamDTO) {
        String key = searchParamDTO.getKey();
        if (StringUtils.isEmpty(key)) {
            //搜索条件为空，返回异常
            throw new LyException("搜索条件不能为空",400);
        }
        //Api DEFAULT_SEARCH_FIELD默认搜索字段，key查询的值
        //searchSourceBuilder.query(QueryBuilders.matchQuery(DEFAULT_SEARCH_FIELD,key));
        return QueryBuilders.matchQuery(DEFAULT_SEARCH_FIELD, key);
    }
    private Goods buildGoods(SpuDTO spu) {
        // 1.自动补全的提示字段
        List<String> suggestion = new ArrayList<>(
                Arrays.asList(StringUtils.split(spu.getCategoryName(), "/")));
        suggestion.add(spu.getName());
        suggestion.add(spu.getBrandName());

        // 2.sku的价格集合
        // 2.1.查询sku集合
        List<SkuDTO> skuList = spu.getSkus();
        if (CollectionUtils.isEmpty(skuList)) {
            // 没有sku，我们去查询
            skuList = itemClient.querySkuBySpuId(spu.getId());
        }
        // 2.2.获取价格集合
        Set<Long> prices = skuList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());

        // 3.商品销量
        long sold = skuList.stream().mapToLong(SkuDTO::getSold).sum();

        // 4.sku的某个图片
        String image = StringUtils.substringBefore(skuList.get(0).getImages(), ",");

        // 5.规格参数
        List<Map<String, Object>> specs = new ArrayList<>();
        // 5.1.查询规格参数name和value键值对，只查询参与搜索的
        List<SpecParamDTO> params = itemClient.querySpecsValues(spu.getId(), true);
        // 5.2.封装
        for (SpecParamDTO param : params) {
            Map<String, Object> map = new HashMap<>(2);
            map.put("name", param.getName());
            map.put("value", chooseSegment(param));
            specs.add(map);
        }

        // 创建Goods对象，并封装数据
        Goods goods = new Goods();
        goods.setUpdateTime(new Date());
        // 自动补全的提示字段
        goods.setSuggestion(suggestion);
        // 规格参数
        goods.setSpecs(specs);
        // 商品销量
        goods.setSold(sold);
        // 商品标题
        goods.setTitle(spu.getTitle() + StringUtils.join(suggestion, " "));
        // sku的价格集合
        goods.setPrices(prices);
        // sku的某个图片
        goods.setImage(image);
        goods.setCategoryId(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setId(spu.getId());
        return goods;
    }
    private Object chooseSegment(SpecParamDTO p) {
        Object value = p.getValue();
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        if (!p.getNumeric() || StringUtils.isBlank(p.getSegments()) || value instanceof Collection) {
            return value;
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }
}
